package heap;

import global.*;
import diskmgr.*;
import java.io.*;
import java.util.*;

public class Heapfile extends Object implements GlobalConst{
	public String name;
	
	public Heapfile(String name) 
			throws HFDiskMgrException, HFException, HFBufMgrException
	{
		boolean newlyCreated = false;
		boolean pinned = false;
		boolean succeed = false;
		
		// Try allocate page
		if(name == null)
			throw new HFException(null, "Heapfile name should not be null!");
		
		// See if the heapfile already exists
		this.name = new String(name);
		PageId headerPageId = null;
		try {
			try {
				if((headerPageId = SystemDefs.JavabaseDB.get_file_entry(name)) != null)
					return;
				
				// Not existing, create a new file
				headerPageId = new PageId();
				SystemDefs.JavabaseDB.allocate_page(headerPageId);
				SystemDefs.JavabaseDB.add_file_entry(name, headerPageId);
			} catch(Exception e) {
				throw new HFDiskMgrException(e, "Heapfile() Failed");
			}
			
			newlyCreated = true;
			
			Page headerPage = new Page();
			try {
				// Initialize the first page
				SystemDefs.JavabaseBM.pinPage(headerPageId, headerPage, false);
			} catch(Exception e) {
				throw new HFBufMgrException(e, "Heapfile() Failed");
			}
			
			pinned = true;
			
			HFPage hfPage = new HFPage(headerPage);
			try {
				hfPage.init(headerPageId, headerPage);
				hfPage.setCurPage(headerPageId);
				hfPage.setNextPage(headerPageId);
				hfPage.setPrevPage(headerPageId);
			} catch(Exception e) {
				throw new HFException(e, "Heapfile() Failed");
			}
			
			succeed = true;
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(pinned) {
					SystemDefs.JavabaseBM.unpinPage(headerPageId, true);
				}
			} catch(Exception e) {
				throw new HFBufMgrException(e, "Heapfile() Failed");
			}
			finally {
				// In case anything bad happens, delete the heapfile if it is newly created
				// no matter if the unpin is successful or not, we'll try delete the file
				if((newlyCreated) && (!succeed)) {
					try {
						SystemDefs.JavabaseDB.delete_file_entry(name);
						SystemDefs.JavabaseDB.deallocate_page(headerPageId);
					}
					catch(Exception e) {
						throw new HFDiskMgrException(e, "Heapfile() Failed and not deleted");
					}
				}
			}
		}
		
	}
	
	public int getRecCnt() 
			throws HFDiskMgrException, HFBufMgrException, HFException
	{
		// To make sure the count is correct even when multiple instance of the same file is open
		// We'll iterate the HFPages to count.
		// An alternative method is to store the record count in the header page, but in this way
		// the header page would be wasted for not being able to store records.
		int recCnt = 0;
		PageId curPageId = new PageId();
		PageId nextPageId = new PageId();
		PageId headerPageId = null;
		Page curPage = new Page();
		HFPage hfPage = null;
		boolean curPagePinned = false;
		try {
			try {
				headerPageId = SystemDefs.JavabaseDB.get_file_entry(name);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "getRecCnt() Failed");
			}
			
			if (headerPageId == null)
				throw new HFException(null, "The heapfile has been deleted already.");
			
			curPageId.copyPageId(headerPageId);
			do {
				// Pin current page
				try {
					SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "getRecCnt() Failed");
				}
				curPagePinned = true;

				// Count the records in current page
				hfPage = new HFPage(curPage);
				try {
					RID curRID = hfPage.firstRecord();
					while(curRID != null) {
						recCnt ++;
						curRID = hfPage.nextRecord(curRID);
					}
					nextPageId.copyPageId(hfPage.getNextPage());
				} catch (Exception e) {
					throw new HFException(e, "getRecCnt() Failed");
				}

				// Unpin current page and go next
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "getRecCnt() Failed");
				}
				curPagePinned = false;
				curPageId.copyPageId(nextPageId);
			} while (curPageId.pid != headerPageId.pid);
		}
		finally {
			// unpin current page if it is not unpinned yet and something bad happened
			if(curPagePinned) {
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "getRecCnt() Failed and page not unpinned");
				}
			}
		}
		
		return recCnt;
	}
	
	public RID insertRecord(byte recPtr[]) 
			throws SpaceNotAvailableException, HFException, 
			HFBufMgrException, HFDiskMgrException
	{
		if (recPtr.length > global.GlobalConst.MINIBASE_PAGESIZE) {
			throw new SpaceNotAvailableException(null, "record larger than page size");
		}
		
		PageId curPageId = new PageId();
		PageId nextPageId = new PageId();
		PageId headerPageId = null;
		Page curPage = new Page();
		HFPage hfPage = null;
		RID recRID = null;
		boolean curPagePinned = false;
		boolean curPageDirty = false;
		boolean newPageAllocated = false;
		boolean succeed = false;
		try {
			try {
				headerPageId = SystemDefs.JavabaseDB.get_file_entry(name);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "insertRecord() Failed");
			}
			
			if (headerPageId == null)
				throw new HFException(null, "The heapfile has been deleted already.");
			
			curPageId.copyPageId(headerPageId);
			// Try find a existing page to insert the Record
			do {
				// Pin current page
				try {
					SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "insertRecord() Failed");
				}
				curPagePinned = true;

				// try insert the records in current page
				hfPage = new HFPage(curPage);
				try {
					recRID = hfPage.insertRecord(recPtr);
					
					// If we succeed insert a record in current page, just return.
					// The "finally statement will unpin the current page.
					if(recRID != null) {
						succeed = true;
						curPageDirty = true;
						return recRID;
					}
					
					nextPageId.copyPageId(hfPage.getNextPage());
				} catch (Exception e) {
					throw new HFException(e, "insertRecord() Failed");
				}

				// Unpin current page and go next
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "insertRecord() Failed");
				}
				curPagePinned = false;
				curPageId.copyPageId(nextPageId);
			} while (curPageId.pid != headerPageId.pid);
			
			// OK, we could not find a existing page to insert this.
			// Try allocate a new page and insert the record in the new page.
			try {
				SystemDefs.JavabaseDB.allocate_page(curPageId);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "insertRecord() Failed");
			}
			newPageAllocated = true;
			
			try {
				SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
			} catch (Exception e) {
				throw new HFBufMgrException(e, "insertRecord() Failed");
			}
			curPagePinned = true;
			
			hfPage = new HFPage(curPage);
			try {
				hfPage.init(curPageId, curPage);
				hfPage.setCurPage(curPageId);
				recRID = hfPage.insertRecord(recPtr);
			} catch (Exception e) {
				throw new HFException(e, "insertRecord() Failed");
			}
			
			// This insert should not fail, but if it does
			// trigger an space not available exception
			if(recRID == null)
				throw new SpaceNotAvailableException(null, "failed to insert record in a new page");
			
			// Link the new page into heapfile
			// rollback is done if anything bad happens
			linkHFPage(headerPageId, curPageId, hfPage);
			
			curPageDirty = true;
			succeed = true;
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(curPagePinned) {
					SystemDefs.JavabaseBM.unpinPage(curPageId, curPageDirty);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "insertRecord() Failed and page not unpinned");
			}
			finally {
				// In case anything bad happens, free the new page if it is newly created
				// no matter if the unpin is successful or not, we'll try free the page
				if((newPageAllocated) && (!succeed)) {
					try {
						SystemDefs.JavabaseBM.freePage(curPageId);
					}
					catch(Exception e) {
						throw new HFBufMgrException(e, "insertRecord() Failed and new page not free");
					}
				}
			}
		}
		
		return recRID;
	}
	
	public boolean deleteRecord(RID rid) 
			throws InvalidSlotNumberException, HFException, 
			HFBufMgrException, HFDiskMgrException
	{
		boolean found = false;
		
		PageId curPageId = new PageId();
		PageId nextPageId = new PageId();
		PageId headerPageId = null;
		Page curPage = new Page();
		HFPage hfPage = null;
		boolean curPagePinned = false;
		boolean curPageDirty = false;
		try {
			try {
				headerPageId = SystemDefs.JavabaseDB.get_file_entry(name);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "deleteRecord() Failed");
			}
			
			if (headerPageId == null)
				throw new HFException(null, "The heapfile has been deleted already.");
			
			curPageId.copyPageId(headerPageId);
			// Try find a existing page to update the Record
			do {
				// Pin current page
				try {
					SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "deleteRecord() Failed");
				}
				curPagePinned = true;

				// try delete the record in current page
				hfPage = new HFPage(curPage);
				try {
					if(curPageId.pid == rid.pageNo.pid) {
						hfPage.deleteRecord(rid);
						
						// Succeed delete a record in current page
						curPageDirty = true;
						found = true;
						break;
					}
					
					nextPageId.copyPageId(hfPage.getNextPage());
				} catch (IOException e) {
					throw new HFException(e, "deleteRecord() Failed");
				} catch (InvalidSlotNumberException e) {
					throw e;
				}

				// Unpin current page and go next
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "deleteRecord() Failed");
				}
				curPagePinned = false;
				curPageId.copyPageId(nextPageId);
			} while (curPageId.pid != headerPageId.pid);
			
			// OK, we could not find a existing page containing this.
			// Just return false.
			if(!found) {
				return false;
			}
			
			// Delete succeed and current page is pinned.
			// Unlink current page if its empty
			hfPage = new HFPage(curPage);
			boolean pageEmpty = false;
			try {
				pageEmpty = hfPage.empty();
			} catch (Exception e) {
				throw new HFException(e, "deleteRecord() Failed");
			}
			
			if(pageEmpty) {
				if (unlinkHFPage(headerPageId, curPageId, hfPage)) {
					// deallocate current page if unlink is successful
					try {
						SystemDefs.JavabaseBM.freePage(curPageId);
					} catch (Exception e) {
						throw new HFBufMgrException(e, "deleteRecord() failed to free empty page");
					}
				}
			}
			
			return true;
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(curPagePinned) {
					SystemDefs.JavabaseBM.unpinPage(curPageId, curPageDirty);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "deleteRecord() Failed and page not unpinned");
			}
		}
	}
	
	public boolean updateRecord(RID rid, Tuple newtuple) 
			throws InvalidSlotNumberException, InvalidUpdateException, 
			HFException, HFDiskMgrException, HFBufMgrException
	{	
		PageId curPageId = new PageId();
		PageId nextPageId = new PageId();
		PageId headerPageId = null;
		Page curPage = new Page();
		HFPage hfPage = null;
		boolean curPagePinned = false;
		boolean curPageDirty = false;
		try {
			try {
				headerPageId = SystemDefs.JavabaseDB.get_file_entry(name);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "updateRecord() Failed");
			}
			
			if (headerPageId == null)
				throw new HFException(null, "The heapfile has been deleted already.");
			
			curPageId.copyPageId(headerPageId);
			// Try find a existing page to update the Record
			do {
				// Pin current page
				try {
					SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "updateRecord() Failed");
				}
				curPagePinned = true;

				// try update the record in current page
				hfPage = new HFPage(curPage);
				try {
					if(curPageId.pid == rid.pageNo.pid) {
						Tuple oldTuple = hfPage.returnRecord(rid);
						
						if(oldTuple.getLength() != newtuple.getLength()) {
							throw new InvalidUpdateException(null, 
									"length of new tuple does not match old tuple");
						}
						
						oldTuple.tupleCopy(newtuple);
						curPageDirty = true;
						return true;
					}
					
					nextPageId.copyPageId(hfPage.getNextPage());
				} catch (IOException e) {
					throw new HFException(e, "updateRecord() Failed");
				} catch (InvalidSlotNumberException e) {
					throw e;
				} catch (InvalidUpdateException e) {
					throw e;
				}

				// Unpin current page and go next
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "updateRecord() Failed");
				}
				curPagePinned = false;
				curPageId.copyPageId(nextPageId);
			} while (curPageId.pid != headerPageId.pid);
			
			// OK, we could not find a existing page containing this.
			// Just return false.
			return false;
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(curPagePinned) {
					SystemDefs.JavabaseBM.unpinPage(curPageId, curPageDirty);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "updateRecord() Failed and page not unpinned");
			}
		}
	}
	
	public Tuple getRecord(RID rid) 
			throws InvalidSlotNumberException, InvalidTupleSizeException, 
			HFException, HFDiskMgrException, HFBufMgrException, Exception
	{
		PageId curPageId = new PageId();
		PageId nextPageId = new PageId();
		PageId headerPageId = null;
		Page curPage = new Page();
		HFPage hfPage = null;
		boolean curPagePinned = false;
		try {
			try {
				headerPageId = SystemDefs.JavabaseDB.get_file_entry(name);
			} catch (Exception e) {
				throw new HFDiskMgrException(e, "getRecord() Failed");
			}
			
			if (headerPageId == null)
				throw new HFException(null, "The heapfile has been deleted already.");
			
			curPageId.copyPageId(headerPageId);
			// Try find a existing page to update the Record
			do {
				// Pin current page
				try {
					SystemDefs.JavabaseBM.pinPage(curPageId, curPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "getRecord() Failed");
				}
				curPagePinned = true;

				// try update the record in current page
				hfPage = new HFPage(curPage);
				try {
					if(curPageId.pid == rid.pageNo.pid) {
						Tuple tuple = hfPage.getRecord(rid);
						return tuple;
					}
					
					nextPageId.copyPageId(hfPage.getNextPage());
				} catch (IOException e) {
					throw new HFException(e, "getRecord() Failed");
				} catch (InvalidSlotNumberException e) {
					throw e;
				}

				// Unpin current page and go next
				try {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "getRecord() Failed");
				}
				curPagePinned = false;
				curPageId.copyPageId(nextPageId);
			} while (curPageId.pid != headerPageId.pid);
			
			// OK, we could not find a existing page containing this.
			// Just return null.
			return null;
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(curPagePinned) {
					SystemDefs.JavabaseBM.unpinPage(curPageId, false);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "getRecord() Failed and page not unpinned");
			}
		}
	}
	
	public Scan openScan() 
			throws HFDiskMgrException, HFException, HFBufMgrException
	{
		return new Scan(this);
	}
	
	public void deleteFile()
			throws InvalidSlotNumberException, FileAlreadyDeletedException, 
			InvalidTupleSizeException, HFBufMgrException, HFDiskMgrException, 
			IOException
	{
		
	}
	
	// The newPage must have been pinned before calling this function!
	private void linkHFPage(PageId headerPageId, PageId newPageId, HFPage newPage) 
			throws HFBufMgrException, HFException {
		// pin both the header page and the last page in the page list (may be the same)
		boolean headerPinned = false;
		boolean tailPinned = false;
		boolean headerDirty = false;
		boolean tailDirty = false;
		PageId tailPageId = new PageId();
		Page headerPage = new Page();
		Page tailPage = new Page();
		try {
			try {
				SystemDefs.JavabaseBM.pinPage(headerPageId, headerPage, false);
			} catch (Exception e) {
				throw new HFBufMgrException(e, "linkHFPage() Failed");
			}
			
			headerPinned = true;
			
			HFPage headerHFPage = new HFPage(headerPage);
			HFPage tailHFPage = null;
			try {
				tailPageId.copyPageId(headerHFPage.getPrevPage());
			} catch (Exception e) {
				throw new HFException(e, "linkHFPage() Failed");
			}
			if(tailPageId.pid == headerPageId.pid) {
				tailHFPage = headerHFPage;
			}
			else {
				try {
					SystemDefs.JavabaseBM.pinPage(tailPageId, tailPage, false);
				} catch (Exception e) {
					throw new HFBufMgrException(e, "linkHFPage() Failed");
				}
				
				tailPinned = true;
				tailHFPage = new HFPage(tailPage);
			}
			
			// OK, all pages needed is ready. Do the modification
			try {
				newPage.setNextPage(headerPageId);
				newPage.setPrevPage(tailPageId);
				headerHFPage.setPrevPage(newPageId);
				headerDirty = true;
				tailHFPage.setNextPage(newPageId);
				tailDirty = true;
			}
			catch(IOException e) {
				// rollback if anything bad happens
				try {
					if(headerDirty)
						headerHFPage.setPrevPage(tailPageId);
					if(tailDirty)
						tailHFPage.setNextPage(headerPageId);
				}
				catch(IOException ex) {
					throw new HFException(ex, "linkHFPage() Failed");
				}
				throw new HFException(e, "linkHFPage() Failed");
			}
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(headerPinned) {
					SystemDefs.JavabaseBM.unpinPage(headerPageId, headerDirty);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "linkHFPage() Failed and page not unpinned");
			}
			finally {
				if(tailPinned) {
					try {
						SystemDefs.JavabaseBM.unpinPage(tailPageId, tailDirty);
					}
					catch(Exception e) {
						throw new HFBufMgrException(e, "linkHFPage() Failed and page not unpinned");
					}
				}
			}
		}
	}
	
	// The newPage must have been pinned before calling this function!
	private boolean unlinkHFPage(PageId headerPageId, PageId curPageId, HFPage curPage) 
			throws HFBufMgrException, HFException {
		// Header could not be unlinked
		if(curPageId == headerPageId)
			return false;
		
		// pin both the page before and after the newpage
		boolean prevPinned = false;
		boolean nextPinned = false;
		boolean prevDirty = false;
		boolean nextDirty = false;
		PageId prevPageId = new PageId();
		PageId nextPageId = new PageId();
		Page prevPage = new Page();
		Page nextPage = new Page();
		try {
			try {
				prevPageId.copyPageId(curPage.getPrevPage());
				nextPageId.copyPageId(curPage.getNextPage());
			}
			catch(IOException ex) {
				throw new HFException(ex, "unlinkHFPage() Failed");
			}
			
			try {
				SystemDefs.JavabaseBM.pinPage(prevPageId, prevPage, false);
				prevPinned = true;
			} catch (Exception e) {
				throw new HFBufMgrException(e, "unlinkHFPage() Failed");
			}

			HFPage prevHFPage = new HFPage(prevPage);
			HFPage nextHFPage = null;
			
			if(nextPageId.pid == prevPageId.pid) {
				nextHFPage = prevHFPage;
			}
			else {
				try {
					SystemDefs.JavabaseBM.pinPage(nextPageId, nextPage, false);
					nextPinned = true;
				} catch (Exception e) {
					throw new HFBufMgrException(e, "unlinkHFPage() Failed");
				}

				nextHFPage = new HFPage(nextPage);
			}
			
			// OK, all pages needed are ready. Do the modification
			try {
				prevHFPage.setNextPage(nextPageId);
				prevDirty = true;
				nextHFPage.setPrevPage(prevPageId);
				nextDirty = true;
			}
			catch(IOException e) {
				// rollback if anything bad happens
				try {
					if(prevDirty)
						prevHFPage.setNextPage(curPageId);
					if(nextDirty)
						nextHFPage.setPrevPage(curPageId);
				}
				catch(IOException ex) {
					throw new HFException(ex, "unlinkHFPage() Failed");
				}
				throw new HFException(e, "unlinkHFPage() Failed");
			}
		}
		finally {
			try {
				// unpin the page if it has been pinned
				if(prevPinned) {
					SystemDefs.JavabaseBM.unpinPage(prevPageId, prevDirty);
				}
			} catch (Exception e) {
				throw new HFBufMgrException(e, "unlinkHFPage() Failed and page not unpinned");
			}
			finally {
				if(nextPinned) {
					try {
						SystemDefs.JavabaseBM.unpinPage(nextPageId, nextDirty);
					}
					catch(Exception e) {
						throw new HFBufMgrException(e, "unlinkHFPage() Failed and page not unpinned");
					}
				}
			}
		}
		
		return true;
	}
}
